CloudFrontの署名付きURL(signed URL)で、「名前をつけて保存」のファイル名を指定する

CloudFrontの署名付きURL(signed URL)で、「名前をつけて保存」のファイル名を指定する

「名前をつけて保存」で任意のファイル名を指定します。
Clock Icon2024.11.11

CloudFrontでは署名付きURL(signed URL)が利用できます。

このとき、「名前をつけて保存」のファイル名を指定する方法を試してみました。

おすすめの方

  • CloudFrontをCloudFormationで作成したい方
  • CloudFrontの署名付きURLを利用したい方
  • CloudFrontの署名付きURLをboto3で発行したい方
  • CloudFrontの署名付きURLで「名前をつけて保存」のファイル名を指定したい方

ライブラリをインストールする

pip install cryptography
pip install boto3

署名の準備をする

公開鍵と秘密鍵を作成し、公開鍵をCloudFrontに登録する

openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

cat public_key.pem | pbcopy

CloudFrontに登録します。

01_cloudfront

キーIDが必要になるので、メモしておきます。

02_cloudfront

パラメータストアにキーIDと秘密鍵を登録する

キーIDをスクリプトに書いたり、秘密鍵をローカルで利用しても良いのですが、せっかくなのでパラメータストアに登録します。 (Lambdaで実行する場合の想定です。)

aws ssm put-parameter \
    --name "/CloudFront/TestKeyId" \
    --type "String" \
    --value "K3HET2C9J3NLWC" \
    --overwrite
aws ssm put-parameter \
    --name "/CloudFront/TestPrivateKey" \
    --type "SecureString" \
    --value file://private_key.pem \
    --overwrite

CloudFrontを作成する

テンプレートファイル

バケットポリシーは、Getのみを指定します。また、CachePolicyでwhitelistに「response-content-disposition」を設定しています。

cloudfront.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: CloudFront Stack

Parameters:
  TestKeyId:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /CloudFront/TestKeyId

Resources:
  TestBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub cloudfront-s3-test-${AWS::AccountId}-${AWS::Region}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  TestBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref TestBucket
      PolicyDocument:
        Id: TestBucket-BucketPolicy
        Statement:
          - Effect: Allow
            Action:
              - s3:GetObject
            Resource:
              - !Sub arn:aws:s3:::${TestBucket}/*
            Principal:
              Service: cloudfront.amazonaws.com
            Condition:
              StringEquals:
                AWS:SourceArn:
                  !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${TestDistribution}

  TestOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: TestOriginAccessControl
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  TestKeyGroup:
    Type: AWS::CloudFront::KeyGroup
    Properties: 
      KeyGroupConfig: 
        Name: test-key-group
        Items:
          - !Ref TestKeyId

  TestDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: !Sub S3-${TestBucket}
            DomainName: !GetAtt TestBucket.RegionalDomainName
            OriginAccessControlId: !GetAtt TestOriginAccessControl.Id
            S3OriginConfig: {}
        Enabled: true
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          TargetOriginId: !Sub S3-${TestBucket}
          AllowedMethods:
            - HEAD
            - GET
          CachePolicyId: !Ref CachePolicy
          ViewerProtocolPolicy: https-only
          TrustedKeyGroups:
            - !Ref TestKeyGroup
        HttpVersion: http2

  CachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties:
      CachePolicyConfig:
        Name: test-cache-policy
        DefaultTTL: 1
        MinTTL: 1
        MaxTTL: 1
        ParametersInCacheKeyAndForwardedToOrigin:
          CookiesConfig:
            CookieBehavior: none
          HeadersConfig:
            HeaderBehavior: none
          QueryStringsConfig:
            QueryStringBehavior: whitelist
            QueryStrings:
              - response-content-disposition
          EnableAcceptEncodingGzip: true

デプロイ

aws cloudformation deploy \
    --template-file cloudfront.yaml \
    --stack-name CloudFront-Test-Stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset

適当なファイルをS3バケットに置く

適当なテキストを作成して、S3バケットに格納します。

echo 'hello, world' > test.txt
aws s3 cp test.txt s3://cloudfront-s3-test-AwsAccountId-ap-northeast-1

11_s3

CloudFrontの署名付きURLを利用して、データの取得を試す

署名付きURLを発行するスクリプト

指定するファイル名を「アルファベットのみ」と「日本語あり」の2種類で試します。

  • hello.txt
  • はろー.txt
app.py
import boto3

from datetime import datetime
from zoneinfo import ZoneInfo
from urllib.parse import quote

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from botocore.signers import CloudFrontSigner

BASE_URL = "https://xxx.cloudfront.net"

ssm = boto3.client("ssm")

def main():
    filename = "hello.txt"
    response_content_disposition_value1 = quote(f'attachment; filename="{filename}"')

    url1 = f"{BASE_URL}/test.txt?response-content-disposition={response_content_disposition_value1}"

    signed_url1 = get_signed_url(
        url1,
        datetime(2024, 11, 8, 21, 00, 0, tzinfo=ZoneInfo("Asia/Tokyo")),
    )

    # -----

    filename_jp = quote("はろー.txt".encode("utf-8"))
    response_content_disposition_value2 = quote(
        f"attachment; filename=\"hello.txt\"; filename*=UTF-8''{filename_jp}"
    )

    url1 = f"{BASE_URL}/test.txt?response-content-disposition={response_content_disposition_value2}"

    signed_url2 = get_signed_url(
        url1,
        datetime(2024, 11, 8, 21, 00, 0, tzinfo=ZoneInfo("Asia/Tokyo")),
    )

    # -----

    with open("test.html", "w") as f:
        f.write(f'<a href="{signed_url1}">Download</a>')
        f.write("<br /> <br />")
        f.write(f'<a href="{signed_url2}">Download</a>')

def rsa_signer(data):
    # https://github.com/boto/boto3/blob/develop/boto3/examples/cloudfront.rst
    res = ssm.get_parameter(Name="/CloudFront/TestPrivateKey", WithDecryption=True)
    private_key = serialization.load_pem_private_key(
        res["Parameter"].get("Value").encode(),
        password=None,
        backend=default_backend(),
    )
    return private_key.sign(data, padding.PKCS1v15(), hashes.SHA1())

def get_key_id():
    res = ssm.get_parameter(Name="/CloudFront/TestKeyId")
    return res["Parameter"].get("Value")

def get_signed_url(target_url, expire_date):
    key_id = get_key_id()

    cloudfront_signer = CloudFrontSigner(key_id, rsa_signer)

    return cloudfront_signer.generate_presigned_url(
        target_url, date_less_than=expire_date
    )

if __name__ == "__main__":
    main()

スクリプトを実行する

python app.py

ブラウザでHTMLファイルを開き、ダウンロードする

21_html

それぞれ、名前をつけて保存時のデフォルトのファイル名が表示されました。

22_download

23_download

さいごに

CloudFrontの署名付きURL(signed URL)で、「名前をつけて保存」のファイル名を指定する方法を試してみました。参考になれば幸いです。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.